const Protocols = {
  VMESS: "vmess",
  VLESS: "vless",
  TROJAN: "trojan",
  SHADOWSOCKS: "shadowsocks",
  DOKODEMO: "dokodemo-door",
  SOCKS: "socks",
  HTTP: "http",
};

const VmessMethods = {
  AES_128_GCM: "aes-128-gcm",
  CHACHA20_POLY1305: "chacha20-poly1305",
  AUTO: "auto",
  NONE: "none",
};

const SSMethods = {
  // AES_256_CFB: 'aes-256-cfb',
  // AES_128_CFB: 'aes-128-cfb',
  // CHACHA20: 'chacha20',
  // CHACHA20_IETF: 'chacha20-ietf',
  CHACHA20_POLY1305: "chacha20-poly1305",
  AES_256_GCM: "aes-256-gcm",
  AES_128_GCM: "aes-128-gcm",
};

const RULE_IP = {
  PRIVATE: "geoip:private",
  CN: "geoip:cn",
};

const RULE_DOMAIN = {
  ADS: "geosite:category-ads",
  ADS_ALL: "geosite:category-ads-all",
  CN: "geosite:cn",
  GOOGLE: "geosite:google",
  FACEBOOK: "geosite:facebook",
  SPEEDTEST: "geosite:speedtest",
};

Object.freeze(Protocols);
Object.freeze(VmessMethods);
Object.freeze(SSMethods);
Object.freeze(RULE_IP);
Object.freeze(RULE_DOMAIN);

class V2CommonClass {
  static toJsonArray(arr) {
    return arr.map((obj) => obj.toJson());
  }

  static toHeaders(v2Headers) {
    let newHeaders = [];
    if (v2Headers) {
      Object.keys(v2Headers).forEach((key) => {
        let values = v2Headers[key];
        if (typeof values === "string") {
          newHeaders.push({ name: key, value: values });
        } else {
          for (let i = 0; i < values.length; ++i) {
            newHeaders.push({ name: key, value: values[i] });
          }
        }
      });
    }
    return newHeaders;
  }

  static toV2Headers(headers, arr = true) {
    let v2Headers = {};
    if (headers) {
      for (let i = 0; i < headers.length; ++i) {
        let name = headers[i].name;
        let value = headers[i].value;
        if (isEmpty(name) || isEmpty(value)) {
          continue;
        }
        if (!(name in v2Headers)) {
          v2Headers[name] = arr ? [value] : value;
        } else {
          if (arr) {
            v2Headers[name].push(value);
          } else {
            v2Headers[name] = value;
          }
        }
      }
    }
    return v2Headers;
  }

  static fromJson() {
    return new V2CommonClass();
  }

  toJson() {
    return this;
  }

  toString(format = true) {
    return format ? JSON.stringify(this.toJson(), null, 2) : JSON.stringify(this.toJson());
  }
}

class Inbound extends V2CommonClass {
  constructor(
    user_id = null,
    port = 1080,
    listen = "0.0.0.0",
    protocol = Protocols.VMESS,
    settings = null,
    streamSettings = new StreamSettings(),
    tag = "",
    sniffing = new Sniffing(),
    remark = "",
    enable = true
  ) {
    super();
    this.user_id = user_id;
    this.port = port;
    this.listen = listen;
    this.protocol = protocol;
    this.settings = isEmpty(settings) ? Inbound.Settings.getSettings(protocol) : settings;
    this.stream = streamSettings;
    this.tag = tag;
    this.sniffing = sniffing;
    this.remark = remark;
    this.enable = enable;
  }

  _genVmessLink(address = "") {
    if (this.protocol !== Protocols.VMESS) {
      return "";
    }
    let network = this.stream.network;
    let type = "none";
    let host = "";
    let path = "";
    if (network === "tcp") {
      let tcp = this.stream.tcp;
      type = tcp.type;
      if (type === "http") {
        let request = tcp.request;
        path = request.path.join(",");
        let index = request.headers.findIndex((header) => header.name.toLowerCase() === "host");
        if (index >= 0) {
          host = request.headers[index].value;
        }
      }
    } else if (network === "kcp") {
      let kcp = this.stream.kcp;
      type = kcp.type;
      path = kcp.seed;
    } else if (network === "ws") {
      let ws = this.stream.ws;
      path = ws.path;
      let index = ws.headers.findIndex((header) => header.name.toLowerCase() === "host");
      if (index >= 0) {
        host = ws.headers[index].value;
      }
    } else if (network === "http") {
      network = "h2";
      path = this.stream.http.path;
      host = this.stream.http.host.join(",");
    } else if (network === "quic") {
      type = this.stream.quic.type;
      host = this.stream.quic.security;
      path = this.stream.quic.key;
    }

    if (this.stream.security === "tls") {
      if (!isEmpty(this.stream.tls.server)) {
        address = this.stream.tls.server;
      }
    }

    let obj = {
      v: "2",
      ps: this.remark,
      add: address,
      port: this.port,
      id: this.settings.vmesses[0].id,
      aid: this.settings.vmesses[0].alterId,
      net: network,
      type: type,
      host: host,
      path: path,
      tls: this.stream.security,
    };
    return "vmess://" + base64(JSON.stringify(obj, null, 2));
  }

  _genVLESSLink(address = "") {
    const settings = this.settings;
    const uuid = settings.vlesses[0].id;
    const port = this.port;
    const type = this.stream.network;
    const params = new Map();
    params.set("type", this.stream.network);
    params.set("security", this.stream.security);
    switch (type) {
      case "tcp":
        const tcp = this.stream.tcp;
        if (tcp.type === "http") {
          const request = tcp.request;
          params.set("path", request.path.join(","));
          const index = request.headers.findIndex((header) => header.name.toLowerCase() === "host");
          if (index >= 0) {
            const host = request.headers[index].value;
            params.set("host", host);
          }
        }
        break;
      case "kcp":
        const kcp = this.stream.kcp;
        params.set("headerType", kcp.type);
        params.set("seed", kcp.seed);
        break;
      case "ws":
        const ws = this.stream.ws;
        params.set("path", ws.path);
        const index = ws.headers.findIndex((header) => header.name.toLowerCase() === "host");
        if (index >= 0) {
          const host = ws.headers[index].value;
          params.set("host", host);
        }
        break;
      case "http":
        const http = this.stream.http;
        params.set("path", http.path);
        params.set("host", http.host);
        break;
      case "quic":
        const quic = this.stream.quic;
        params.set("quicSecurity", quic.security);
        params.set("key", quic.key);
        params.set("headerType", quic.type);
        break;
    }

    if (this.stream.security === "tls") {
      if (!isEmpty(this.stream.tls.server)) {
        address = this.stream.tls.server;
        params.set("sni", address);
      }
    }

    for (const [key, value] of params) {
      switch (key) {
        case "host":
        case "path":
        case "seed":
        case "key":
        case "alpn":
          params.set(key, encodeURIComponent(value));
          break;
      }
    }

    const link = `vless://${uuid}@${address}:${port}`;
    const url = new URL(link);
    for (const [key, value] of params) {
      url.searchParams.set(key, value);
    }
    return url.toString();
  }

  _genSSLink(address = "") {
    let settings = this.settings;
    const server = this.stream.tls.server;
    if (!isEmpty(server)) {
      address = server;
    }
    return (
      "ss://" +
      safeBase64(settings.method + ":" + settings.password + "@" + address + ":" + this.port) +
      "#" +
      encodeURIComponent(this.remark)
    );
  }

  _genTrojanLink(address = "") {
    let settings = this.settings;
    return `trojan://${settings.clients[0].password}@${address}:${this.port}#${encodeURIComponent(
      this.remark
    )}`;
  }

  genLink(address = "") {
    switch (this.protocol) {
      case Protocols.VMESS:
        return this._genVmessLink(address);
      case Protocols.VLESS:
        return this._genVLESSLink(address);
      case Protocols.SHADOWSOCKS:
        return this._genSSLink(address);
      case Protocols.TROJAN:
        return this._genTrojanLink(address);
      default:
        return "";
    }
  }

  static fromJson(json = {}) {
    return new Inbound(
      json.user_id,
      json.port,
      json.listen,
      json.protocol,
      Inbound.Settings.fromJson(json.protocol, json.settings),
      StreamSettings.fromJson(json.streamSettings),
      json.tag,
      Sniffing.fromJson(json.sniffing),
      json.remark,
      json.enable
    );
  }

  toJson() {
    let streamSettings;
    if (
      this.protocol === Protocols.VMESS ||
      this.protocol === Protocols.VLESS ||
      this.protocol === Protocols.TROJAN ||
      this.protocol === Protocols.SHADOWSOCKS
    ) {
      streamSettings = this.stream.toJson();
    }
    return {
      user_id: this.user_id,
      port: this.port,
      listen: this.listen,
      protocol: this.protocol,
      settings: this.settings instanceof V2CommonClass ? this.settings.toJson() : this.settings,
      streamSettings: streamSettings,
      tag: this.tag,
      sniffing: this.sniffing.toJson(),
      remark: this.remark,
      enable: this.enable,
    };
  }
}

Inbound.Settings = class extends V2CommonClass {
  constructor(protocol) {
    super();
    this.protocol = protocol;
  }

  static getSettings(protocol) {
    switch (protocol) {
      case Protocols.VMESS:
        return new Inbound.VmessSettings(protocol);
      case Protocols.VLESS:
        return new Inbound.VlessSettings(protocol);
      case Protocols.TROJAN:
        return new Inbound.TrojanSettings(protocol);
      case Protocols.SHADOWSOCKS:
        return new Inbound.ShadowsocksSettings(protocol);
      case Protocols.DOKODEMO:
        return new Inbound.DokodemoSettings(protocol);
      case Protocols.SOCKS:
        return new Inbound.SocksSettings(protocol);
      case Protocols.HTTP:
        return new Inbound.HttpSettings(protocol);
      default:
        return null;
    }
  }

  static fromJson(protocol, json) {
    switch (protocol) {
      case Protocols.VMESS:
        return Inbound.VmessSettings.fromJson(json);
      case Protocols.VLESS:
        return Inbound.VlessSettings.fromJson(json);
      case Protocols.TROJAN:
        return Inbound.TrojanSettings.fromJson(json);
      case Protocols.SHADOWSOCKS:
        return Inbound.ShadowsocksSettings.fromJson(json);
      case Protocols.DOKODEMO:
        return Inbound.DokodemoSettings.fromJson(json);
      case Protocols.SOCKS:
        return Inbound.SocksSettings.fromJson(json);
      case Protocols.HTTP:
        return Inbound.HttpSettings.fromJson(json);
      default:
        return null;
    }
  }

  toJson() {
    switch (protocol) {
      case Protocols.VMESS:
        return Inbound.VmessSettings.toJson();
      case Protocols.VLESS:
        return Inbound.VlessSettings.toJson();
      case Protocols.TROJAN:
        return Inbound.TrojanSettings.toJson();
      case Protocols.SHADOWSOCKS:
        return Inbound.ShadowsocksSettings.toJson();
      case Protocols.DOKODEMO:
        return Inbound.DokodemoSettings.toJson();
      case Protocols.SOCKS:
        return Inbound.SocksSettings.toJson();
      case Protocols.HTTP:
        return Inbound.HttpSettings.toJson();
      default:
        return null;
    }
  }
};

Inbound.VmessSettings = class extends Inbound.Settings {
  constructor(
    protocol,
    vmesses = [new Inbound.VmessSettings.Vmess()],
    disableInsecureEncryption = false
  ) {
    super(protocol);
    this.vmesses = vmesses;
    this.disableInsecure = disableInsecureEncryption;
  }

  indexOfVmessById(id) {
    return this.vmesses.findIndex((vmess) => vmess.id === id);
  }

  addVmess(vmess) {
    if (this.indexOfVmessById(vmess.id) >= 0) return false;
    this.vmesses.push(vmess);
  }

  delVmess(vmess) {
    const i = this.indexOfVmessById(vmess.id);
    if (i >= 0) {
      this.vmesses.splice(i, 1);
    }
  }

  static fromJson(json = {}) {
    return new Inbound.VmessSettings(
      Protocols.VMESS,
      json.clients.map((client) => Inbound.VmessSettings.Vmess.fromJson(client)),
      isEmpty(json.disableInsecureEncryption) ? false : json.disableInsecureEncryption
    );
  }

  toJson() {
    return {
      clients: Inbound.VmessSettings.toJsonArray(this.vmesses),
      disableInsecureEncryption: this.disableInsecure,
    };
  }
};

Inbound.VmessSettings.Vmess = class extends V2CommonClass {
  constructor(id = randomUUID(), level = 0, alterId = 0, email = "") {
    super();
    this.id = id;
    this.level = level;
    this.alterId = alterId;
    this.email = email;
  }

  static fromJson(json = {}) {
    return new Inbound.VmessSettings.Vmess(json.id, json.level, json.alterId, json.email);
  }
};

Inbound.VlessSettings = class extends Inbound.Settings {
  constructor(
    protocol,
    vlesses = [new Inbound.VlessSettings.Vless()],
    decryption = "none",
    fallbacks = []
  ) {
    super(protocol);
    this.vlesses = vlesses;
    this.decryption = decryption;
    this.fallbacks = fallbacks;
  }

  indexOfVlessById(id) {
    return this.velsses.findIndex((vless) => vless.id === id);
  }

  addVless(vless) {
    if (this.indexOfVmessById(vless.id) >= 0) return false;
    this.velsses.push(vless);
  }

  delVless(vless) {
    const i = this.indexOfVmessById(vless.id);
    if (i >= 0) {
      this.velsses.splice(i, 1);
    }
  }

  addFallback(name, alpn, path, dest, xver) {
    this.fallbacks.push(new Inbound.VlessSettings.Fallback(name, alpn, path, dest, xver));
  }

  removeFallback(index) {
    this.fallbacks.splice(index, 1);
  }

  static fromJson(json = {}) {
    return new Inbound.VlessSettings(
      Protocols.VLESS,
      json.clients.map((client) => Inbound.VlessSettings.Vless.fromJson(client)),
      json.decryption,
      Inbound.VlessSettings.Fallback.fromJson(json.fallbacks)
    );
  }

  toJson() {
    return {
      clients: Inbound.VlessSettings.toJsonArray(this.vlesses),
      decryption: this.decryption,
      fallbacks: Inbound.VlessSettings.toJsonArray(this.fallbacks),
    };
  }
};

Inbound.VlessSettings.Vless = class extends V2CommonClass {
  constructor(id = randomUUID(), level = 0, email = "", flow = "") {
    super();
    this.id = id;
    this.level = level;
    this.email = email;
    this.flow = flow;
  }

  static fromJson(json = {}) {
    return new Inbound.VlessSettings.Vless(json.id, json.level, json.email, json.flow);
  }
};

Inbound.VlessSettings.Fallback = class extends V2CommonClass {
  constructor(name = "", alpn = "", path = "", dest = 80, xver = 0) {
    super();
    this.name = name;
    this.alpn = alpn;
    this.path = path;
    this.dest = dest;
    this.xver = xver;
  }

  toJson() {
    return {
      name: this.name,
      alpn: this.alpn,
      path: this.path,
      dest: this.dest,
      xver: this.xver,
    };
  }

  static fromJson(json = []) {
    const fallbacks = [];
    for (let fallback of json) {
      fallbacks.push(
        new Inbound.VlessSettings.Fallback(
          fallback.name,
          fallback.alpn,
          fallback.path,
          fallback.dest,
          fallback.xver
        )
      );
    }
    return fallbacks;
  }
};

Inbound.TrojanSettings = class extends Inbound.Settings {
  constructor(protocol, clients = [new Inbound.TrojanSettings.Client()]) {
    super(protocol);
    this.clients = clients;
  }

  indexOfTrojanById(id) {
    // ...
  }

  addTrojan(trojan) {
    // ...
  }

  delTrojan(trojan) {
    // ...
  }

  toJson() {
    return {
      clients: Inbound.TrojanSettings.toJsonArray(this.clients),
    };
  }

  static fromJson(json = {}) {
    const clients = [];
    for (const c of json.clients) {
      clients.push(Inbound.TrojanSettings.Client.fromJson(c));
    }
    return new Inbound.TrojanSettings(Protocols.TROJAN, clients);
  }
};

Inbound.TrojanSettings.Client = class extends V2CommonClass {
  constructor(password = randomString(10)) {
    super();
    this.password = password;
  }

  toJson() {
    return {
      password: this.password,
    };
  }

  static fromJson(json = {}) {
    return new Inbound.TrojanSettings.Client(json.password);
  }
};

Inbound.ShadowsocksSettings = class extends Inbound.Settings {
  constructor(
    protocol,
    method = SSMethods.AES_256_GCM,
    password = randomString(10),
    network = "tcp,udp"
  ) {
    super(protocol);
    this.method = method;
    this.password = password;
    this.network = network;
  }

  static fromJson(json = {}) {
    return new Inbound.ShadowsocksSettings(
      Protocols.SHADOWSOCKS,
      json.method,
      json.password,
      json.network
    );
  }

  toJson() {
    return {
      method: this.method,
      password: this.password,
      network: this.network,
    };
  }
};

Inbound.DokodemoSettings = class extends Inbound.Settings {
  constructor(protocol, address, port, network = "tcp,udp") {
    super(protocol);
    this.address = address;
    this.port = port;
    this.network = network;
  }

  static fromJson(json = {}) {
    return new Inbound.DokodemoSettings(Protocols.DOKODEMO, json.address, json.port, json.network);
  }

  toJson() {
    return {
      address: this.address,
      port: this.port,
      network: this.network,
    };
  }
};

Inbound.SocksSettings = class extends Inbound.Settings {
  constructor(
    protocol,
    auth = "password",
    accounts = [new Inbound.SocksSettings.SocksAccount()],
    udp = false,
    ip = "127.0.0.1"
  ) {
    super(protocol);
    this.auth = auth;
    this.accounts = accounts;
    this.udp = udp;
    this.ip = ip;
  }

  addAccount(account) {
    this.accounts.push(account);
  }

  delAccount(index) {
    this.accounts.splice(index, 1);
  }

  static fromJson(json = {}) {
    let accounts;
    if (json.auth === "password") {
      accounts = json.accounts.map((account) =>
        Inbound.SocksSettings.SocksAccount.fromJson(account)
      );
    }
    return new Inbound.SocksSettings(Protocols.SOCKS, json.auth, accounts, json.udp, json.ip);
  }

  toJson() {
    return {
      auth: this.auth,
      accounts:
        this.auth === "password" ? this.accounts.map((account) => account.toJson()) : undefined,
      udp: this.udp,
      ip: this.ip,
    };
  }
};

Inbound.SocksSettings.SocksAccount = class extends V2CommonClass {
  constructor(user = randomString(10), pass = randomString(10)) {
    super();
    this.user = user;
    this.pass = pass;
  }

  static fromJson(json = {}) {
    return new Inbound.SocksSettings.SocksAccount(json.user, json.pass);
  }
};

Inbound.HttpSettings = class extends Inbound.Settings {
  constructor(protocol, accounts = [new Inbound.HttpSettings.HttpAccount()]) {
    super(protocol);
    this.accounts = accounts;
  }

  addAccount(account) {
    this.accounts.push(account);
  }

  delAccount(index) {
    this.accounts.splice(index, 1);
  }

  static fromJson(json = {}) {
    return new Inbound.HttpSettings(
      Protocols.HTTP,
      json.accounts.map((account) => Inbound.HttpSettings.HttpAccount.fromJson(account))
    );
  }

  toJson() {
    return {
      accounts: Inbound.HttpSettings.toJsonArray(this.accounts),
    };
  }
};

Inbound.HttpSettings.HttpAccount = class extends V2CommonClass {
  constructor(user = randomString(10), pass = randomString(10)) {
    super();
    this.user = user;
    this.pass = pass;
  }

  static fromJson(json = {}) {
    return new Inbound.HttpSettings.HttpAccount(json.user, json.pass);
  }
};

class StreamSettings extends V2CommonClass {
  constructor(
    network = "tcp",
    security = "none",
    tcpSettings = new TcpStreamSettings(),
    tlsSettings = new TlsStreamSettings(),
    xtlsSettings = new XtlsStreamSettings(),
    kcpSettings = new KcpStreamSettings(),
    wsSettings = new WsStreamSettings(),
    httpSettings = new HttpStreamSettings(),
    quicSettings = new QuicStreamSettings()
  ) {
    super();
    this.network = network;
    this.security = security;
    this.tcp = tcpSettings;
    this.tls = tlsSettings;
    this.xtls = xtlsSettings;
    this.kcp = kcpSettings;
    this.ws = wsSettings;
    this.http = httpSettings;
    this.quic = quicSettings;
  }

  static fromJson(json = {}) {
    return new StreamSettings(
      json.network,
      json.security,
      TcpStreamSettings.fromJson(json.tcpSettings),
      TlsStreamSettings.fromJson(json.tlsSettings),
      XtlsStreamSettings.fromJson(json.xtlsSettings),
      KcpStreamSettings.fromJson(json.kcpSettings),
      WsStreamSettings.fromJson(json.wsSettings),
      HttpStreamSettings.fromJson(json.httpSettings),
      QuicStreamSettings.fromJson(json.quicSettings)
    );
  }

  toJson() {
    let network = this.network;
    let security = this.security;
    return {
      network: network,
      security: security,
      tcpSettings: network === "tcp" ? this.tcp.toJson() : undefined,
      tlsSettings:
        security === "tls" && ["tcp", "ws", "http", "quic"].indexOf(network) >= 0
          ? this.tls.toJson()
          : undefined,
      xtlsSettings:
        security === "xtls" && ["tcp", "ws", "http", "quic"].indexOf(network) >= 0
          ? this.xtls.toJson()
          : undefined,
      kcpSettings: network === "kcp" ? this.kcp.toJson() : undefined,
      wsSettings: network === "ws" ? this.ws.toJson() : undefined,
      httpSettings: network === "http" ? this.http.toJson() : undefined,
      quicSettings: network === "quic" ? this.quic.toJson() : undefined,
    };
  }
}

class TcpStreamSettings extends V2CommonClass {
  constructor(
    type = "none",
    request = new TcpStreamSettings.TcpRequest(),
    response = new TcpStreamSettings.TcpResponse()
  ) {
    super();
    this.type = type;
    this.request = request;
    this.response = response;
  }

  static fromJson(json = {}) {
    let header = json.header;
    if (!header) {
      header = {};
    }
    return new TcpStreamSettings(
      header.type,
      TcpStreamSettings.TcpRequest.fromJson(header.request),
      TcpStreamSettings.TcpResponse.fromJson(header.response)
    );
  }

  toJson() {
    return {
      header: {
        type: this.type,
        request: this.type === "http" ? this.request.toJson() : undefined,
        response: this.type === "http" ? this.response.toJson() : undefined,
      },
    };
  }
}

TcpStreamSettings.TcpRequest = class extends V2CommonClass {
  constructor(version = "1.1", method = "GET", path = ["/"], headers = []) {
    super();
    this.version = version;
    this.method = method;
    this.path = path.length === 0 ? ["/"] : path;
    this.headers = headers;
  }

  addPath(path) {
    this.path.push(path);
  }

  removePath(index) {
    this.path.splice(index, 1);
  }

  addHeader(name, value) {
    this.headers.push({ name: name, value: value });
  }

  removeHeader(index) {
    this.headers.splice(index, 1);
  }

  static fromJson(json = {}) {
    return new TcpStreamSettings.TcpRequest(
      json.version,
      json.method,
      json.path,
      V2CommonClass.toHeaders(json.headers)
    );
  }

  toJson() {
    return {
      method: this.method,
      path: clone(this.path),
      headers: V2CommonClass.toV2Headers(this.headers),
    };
  }
};

TcpStreamSettings.TcpResponse = class extends V2CommonClass {
  constructor(version = "1.1", status = "200", reason = "OK", headers = []) {
    super();
    this.version = version;
    this.status = status;
    this.reason = reason;
    this.headers = headers;
  }

  addHeader(name, value) {
    this.headers.push({ name: name, value: value });
  }

  removeHeader(index) {
    this.headers.splice(index, 1);
  }

  static fromJson(json = {}) {
    return new TcpStreamSettings.TcpResponse(
      json.version,
      json.status,
      json.reason,
      V2CommonClass.toHeaders(json.headers)
    );
  }

  toJson() {
    return {
      version: this.version,
      status: this.status,
      reason: this.reason,
      headers: V2CommonClass.toV2Headers(this.headers),
    };
  }
};

class TlsStreamSettings extends V2CommonClass {
  constructor(serverName = "", certificates = [new TlsStreamSettings.Cert()]) {
    super();
    this.server = serverName;
    this.certs = certificates;
  }

  addCert(cert) {
    this.certs.push(cert);
  }

  removeCert(index) {
    this.certs.splice(index, 1);
  }

  static fromJson(json = {}) {
    let certs;
    if (!isEmpty(json.certificates)) {
      certs = json.certificates.map((cert) => TlsStreamSettings.Cert.fromJson(cert));
    }
    return new TlsStreamSettings(json.serverName, certs);
  }

  toJson() {
    return {
      serverName: this.server,
      certificates: TlsStreamSettings.toJsonArray(this.certs),
    };
  }
}

TlsStreamSettings.Cert = class extends V2CommonClass {
  constructor(useFile = true, certificateFile = "", keyFile = "", certificate = "", key = "") {
    super();
    this.useFile = useFile;
    this.certFile = certificateFile;
    this.keyFile = keyFile;
    this.cert = certificate instanceof Array ? certificate.join("\n") : certificate;
    this.key = key instanceof Array ? key.join("\n") : key;
  }

  static fromJson(json = {}) {
    if ("certificateFile" in json && "keyFile" in json) {
      return new TlsStreamSettings.Cert(true, json.certificateFile, json.keyFile);
    } else {
      return new TlsStreamSettings.Cert(
        false,
        "",
        "",
        json.certificate.join("\n"),
        json.key.join("\n")
      );
    }
  }

  toJson() {
    if (this.useFile) {
      return {
        certificateFile: this.certFile,
        keyFile: this.keyFile,
      };
    } else {
      return {
        certificate: this.cert.split("\n"),
        key: this.key.split("\n"),
      };
    }
  }
};

class XtlsStreamSettings extends V2CommonClass {
  constructor(alpn = ["http/1.1"], certificates = [new XtlsStreamSettings.Cert()]) {
    super();
    this.alpn = alpn;
    this.certs = certificates;
  }

  addCert(cert) {
    this.certs.push(cert);
  }

  removeCert(index) {
    this.certs.splice(index, 1);
  }

  static fromJson(json = {}) {
    let certs;
    if (!isEmpty(json.certificates)) {
      certs = json.certificates.map((cert) => XtlsStreamSettings.Cert.fromJson(cert));
    }
    return new XtlsStreamSettings(json.alpn, certs);
  }

  toJson() {
    return {
      alpn: this.alpn,
      certificates: XtTlsStreamSettings.toJsonArray(this.certs),
    };
  }
}

XtlsStreamSettings.Cert = class extends V2CommonClass {
  constructor(useFile = true, certificateFile = "", keyFile = "", certificate = "", key = "") {
    super();
    this.useFile = useFile;
    this.certFile = certificateFile;
    this.keyFile = keyFile;
    this.cert = certificate instanceof Array ? certificate.join("\n") : certificate;
    this.key = key instanceof Array ? key.join("\n") : key;
  }

  static fromJson(json = {}) {
    if ("certificateFile" in json && "keyFile" in json) {
      return new XtlsStreamSettings.Cert(true, json.certificateFile, json.keyFile);
    } else {
      return new XtlsStreamSettings.Cert(
        false,
        "",
        "",
        json.certificate.join("\n"),
        json.key.join("\n")
      );
    }
  }

  toJson() {
    if (this.useFile) {
      return {
        certificateFile: this.certFile,
        keyFile: this.keyFile,
      };
    } else {
      return {
        certificate: this.cert.split("\n"),
        key: this.key.split("\n"),
      };
    }
  }
};

class KcpStreamSettings extends V2CommonClass {
  constructor(
    mtu = 1350,
    tti = 20,
    uplinkCapacity = 5,
    downlinkCapacity = 20,
    congestion = false,
    readBufferSize = 2,
    writeBufferSize = 2,
    type = "none",
    seed = randomString(10)
  ) {
    super();
    this.mtu = mtu;
    this.tti = tti;
    this.upCap = uplinkCapacity;
    this.downCap = downlinkCapacity;
    this.congestion = congestion;
    this.readBuffer = readBufferSize;
    this.writeBuffer = writeBufferSize;
    this.type = type;
    this.seed = seed;
  }

  static fromJson(json = {}) {
    return new KcpStreamSettings(
      json.mtu,
      json.tti,
      json.uplinkCapacity,
      json.downlinkCapacity,
      json.congestion,
      json.readBufferSize,
      json.writeBufferSize,
      isEmpty(json.header) ? "none" : json.header.type,
      json.seed
    );
  }

  toJson() {
    return {
      mtu: this.mtu,
      tti: this.tti,
      uplinkCapacity: this.upCap,
      downlinkCapacity: this.downCap,
      congestion: this.congestion,
      readBufferSize: this.readBuffer,
      writeBufferSize: this.writeBuffer,
      header: {
        type: this.type,
      },
      seed: this.seed,
    };
  }
}

class WsStreamSettings extends V2CommonClass {
  constructor(path = "/", headers = []) {
    super();
    this.path = path;
    this.headers = headers;
  }

  addHeader(name, value) {
    this.headers.push({ name: name, value: value });
  }

  removeHeader(index) {
    this.headers.splice(index, 1);
  }

  static fromJson(json = {}) {
    return new WsStreamSettings(json.path, V2CommonClass.toHeaders(json.headers));
  }

  toJson() {
    return {
      path: this.path,
      headers: V2CommonClass.toV2Headers(this.headers, false),
    };
  }
}

class HttpStreamSettings extends V2CommonClass {
  constructor(path = "/", host = [""]) {
    super();
    this.path = path;
    this.host = host.length === 0 ? [""] : host;
  }

  addHost(host) {
    this.host.push(host);
  }

  removeHost(index) {
    this.host.splice(index, 1);
  }

  static fromJson(json = {}) {
    return new HttpStreamSettings(json.path, json.host);
  }

  toJson() {
    let host = [];
    for (let i = 0; i < this.host.length; ++i) {
      if (!isEmpty(this.host[i])) {
        host.push(this.host[i]);
      }
    }
    return {
      path: this.path,
      host: host,
    };
  }
}

class QuicStreamSettings extends V2CommonClass {
  constructor(security = VmessMethods.NONE, key = "", type = "none") {
    super();
    this.security = security;
    this.key = key;
    this.type = type;
  }

  static fromJson(json = {}) {
    return new QuicStreamSettings(json.security, json.key, json.header ? json.header.type : "none");
  }

  toJson() {
    return {
      security: this.security,
      key: this.key,
      header: {
        type: this.type,
      },
    };
  }
}

class Sniffing extends V2CommonClass {
  constructor(enabled = true, destOverride = ["http", "tls"]) {
    super();
    this.enabled = enabled;
    this.destOverride = destOverride;
  }

  static fromJson(json = {}) {
    let destOverride = clone(json.destOverride);
    if (!isEmpty(destOverride) && !isArrEmpty(destOverride)) {
      if (isEmpty(destOverride[0])) {
        destOverride = ["http", "tls"];
      }
    }
    return new Sniffing(!!json.enabled, destOverride);
  }
}
